/*
 * Copyright 2011 Alibaba.com All right reserved. This software is the confidential and proprietary information of
 * Alibaba.com ("Confidential Information"). You shall not disclose such Confidential Information and shall use it only
 * in accordance with the terms of the license agreement you entered into with Alibaba.com.
 */
package com.goldplusgold.network.lib.proxy;


import android.text.TextUtils;
import android.util.Log;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.goldplusgold.network.lib.AppCollectedInfoUtil;
import com.goldplusgold.network.lib.annotation._HTTP_DELETE;
import com.goldplusgold.network.lib.annotation._HTTP_GET;
import com.goldplusgold.network.lib.annotation._HTTP_HEAD_PARAM;
import com.goldplusgold.network.lib.annotation._HTTP_PARAM;
import com.goldplusgold.network.lib.annotation._HTTP_POST;
import com.goldplusgold.network.lib.annotation._HTTP_PUT;
import com.goldplusgold.network.lib.annotation._HTTP_URL;
import com.goldplusgold.network.lib.exception.InvokeException;
import com.goldplusgold.network.lib.exception.ServerException;
import com.goldplusgold.network.lib.model.ErrorMessage;
import com.zhy.http.okhttp.OkHttpUtils;

import java.io.UnsupportedEncodingException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import okhttp3.FormBody;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okhttp3.Response;

/**
 * Dynamic Proxy Invocation Handler
 *
 * @author guangming.chen 2013-7-28 下午04:47:56
 */
public class ProxyInvocationHandler implements InvocationHandler {

    private static final String TAG = "ProxyInvocationHandler";
    public static final MediaType mediaType = MediaType.parse("application/json; charset=utf-8");

    public ProxyInvocationHandler() {
    }

    public Object bind(Class<?> clazz) {
        Class<?>[] clazzs = {clazz};
        Object newProxyInstance = Proxy.newProxyInstance(clazz.getClassLoader(), clazzs, this);
        return newProxyInstance;
    }

    /**
     * Dynamic proxy invoke
     */
    public Object invoke(Object proxy, final Method method, Object[] args) throws Throwable {

        _HTTP_POST httpPost = method.getAnnotation(_HTTP_POST.class);
        _HTTP_GET httpGet = method.getAnnotation(_HTTP_GET.class);
        _HTTP_URL httpUrl = method.getAnnotation(_HTTP_URL.class);
        _HTTP_DELETE httpDelete = method.getAnnotation(_HTTP_DELETE.class);
        _HTTP_PUT httpPut = method.getAnnotation(_HTTP_PUT.class);

        Annotation[][] annos = method.getParameterAnnotations();
        String invokeUrl = httpUrl.url();

        HashMap<String, String> params = new HashMap<>();
        HashMap<String, String> headParams = AppCollectedInfoUtil.getAppDefaultParams(false);

        // AkApiParams to hashmap, filter out of null-value
        HashMap<String, String> paramsMapOri = new HashMap<>();
        HashMap<String, String> paramsMap = getRawApiParams2HashMap(annos, args, paramsMapOri);

        // parse '{}'s in url
        invokeUrl = parseUrlbyParams(invokeUrl, paramsMap);

        // add query params
        params.putAll(paramsMap);


        // get the default string if using
        _HTTP_HEAD_PARAM headDefaultParams = method.getAnnotation(_HTTP_HEAD_PARAM.class);
        HashMap<String, String> paramsValue = new HashMap<>();
        if (headDefaultParams != null) {
            Class<?> clazzDefaultParams = headDefaultParams.using();
            if (clazzDefaultParams.getInterfaces().length > 0) {
                InvokeDefaultParams idp = (InvokeDefaultParams) clazzDefaultParams.getConstructors()[0].newInstance();
                paramsValue = idp.getDefaultParamsValue(httpPost != null);
            }

            headParams.putAll(paramsValue);
        }

        Response response;
        if (httpGet != null) {
            response = OkHttpUtils.get()
                    .url(invokeUrl)
                    .headers(headParams)
                    .params(params)
                    .build()
                    .execute();
        } else if (httpDelete != null) {
            invokeUrl = invokeUrl + getUrlParams(headParams, params);
            response = OkHttpUtils.delete()
                    .url(invokeUrl)
                    .headers(headParams)
                    .build()
                    .execute();
        } else if (httpPut != null) {
            FormBody.Builder builder = new FormBody.Builder();
            if (!params.isEmpty()) {
                for (Map.Entry<String, String> item : params.entrySet()) {
                    builder.add(item.getKey(), item.getValue());
                }
            }

            RequestBody body = builder.build();
            response = OkHttpUtils.put()
                    .url(invokeUrl)
                    .headers(headParams)
                    .requestBody(body)
                    .build()
                    .execute();

        } else {
            response = OkHttpUtils.post()
                    .url(invokeUrl)
                    .headers(headParams)
                    .params(params)
                    .build()
                    .execute();
        }

        String content = response.body().string();

        if (TextUtils.isEmpty(content) || response.code() != 200) {
            return null;
        }
        if (content.contains("errorCode")) {
            ErrorMessage errorMessage = JSON.parseObject(content, ErrorMessage.class);
            throw new ServerException(errorMessage.getErrorCode(), errorMessage.getErrorMsg());
        }
        JSONObject jsonObject = JSON.parseObject(content);
        String header = response.header("_x-access-token");
        if (!TextUtils.isEmpty(header)) {
            jsonObject.put("token", header);
            content = jsonObject.toJSONString();
        }
        Class<?> returnType = method.getReturnType();
        if (String.class.equals(returnType)) { // the result return raw string
            return content;
        } else { // return object using json decode
            return JSON.parseObject(content, returnType);
        }

    }


    private String getUrlParams(HashMap<String, String> headParams, HashMap<String, String> bodyParams) {
        if (headParams.isEmpty() && bodyParams.isEmpty()) {
            return "";
        }
        StringBuilder content = new StringBuilder("?");
        Set<String> headKeys = headParams.keySet();
        for (Iterator<String> it = headKeys.iterator(); it.hasNext(); ) {
            String headKey = it.next();
            content.append(headKey);
            content.append("=");
            content.append(headParams.get(headKey));
            content.append("&");
        }

        Set<String> bodyKeys = bodyParams.keySet();
        for (Iterator<String> it = bodyKeys.iterator(); it.hasNext(); ) {
            String headKey = it.next();
            content.append(headKey);
            content.append("=");
            content.append(bodyParams.get(headKey));
            content.append("&");
        }
        if (content.length() > 0) {
            content.deleteCharAt(content.length() - 1);
        }
        return content.toString();
    }


    /**
     * Replace all the {} block in url to the actual params, clear the params used in {block},
     * return
     * cleared params HashMap and replaced url.
     *
     * @param url    such as http://server/{namespace}/1/do
     * @param params such as hashmap include (namespace->'mobile')
     * @return the parsed param will be removed in HashMap (params)
     */
    private String parseUrlbyParams(String url, HashMap<String, String> params) throws InvokeException {

        StringBuffer sbUrl = new StringBuffer();
        Pattern pattern = Pattern.compile("\\{(.+?)\\}");
        Matcher matcher = pattern.matcher(url);

        while (matcher.find()) {
            String paramValue = params.get(matcher.group(1));
            if (paramValue != null) {
                matcher.appendReplacement(sbUrl, paramValue);
            } else { // 对于{name}没有匹配到的则抛出异常
                throw new InvokeException(InvokeException.CODE_PARAM_IN_URL_NOT_FOUND, "Parameter {" + matcher.group(1)
                        + "}'s value not found of url "
                        + url + ".");
            }
            params.remove(matcher.group(1));
        }
        matcher.appendTail(sbUrl);

        return sbUrl.toString();
    }


    /**
     * AkApiParams to hashmap, filter out of null-value
     *
     * @param annosArr Method's params' annotation array[][]
     * @param args     Method's params' values
     * @return HashMap all (paramName -> paramValue)
     */
    @SuppressWarnings("unchecked")
    private HashMap<String, String> getRawApiParams2HashMap(Annotation[][] annosArr, Object[] args,
                                                            HashMap<String, String> paramsMapOri) {
        HashMap<String, String> paramsMap = new HashMap<String, String>();

        if (args == null) {
            return paramsMap;
        }

        for (int idx = 0; idx < args.length; idx++) {
            String paramName = null;
            String encode = "none";
            for (Annotation a : annosArr[idx]) {
                if (_HTTP_PARAM.class.equals(a.annotationType())) {
                    _HTTP_PARAM ap = (_HTTP_PARAM) a;
                    paramName = ap.value();
                    encode = ap.encode();
                }
            }
            if (paramName != null) {
                Object arg = args[idx];
                if (arg != null) { // filter out of null-value param
                    if (encode != null && !"none".equals(encode)) {
                        try {
                            paramsMap.put(paramName, URLEncoder.encode(arg.toString(), encode));
                        } catch (UnsupportedEncodingException e) {
                            Log.e(TAG, "UnsupportedEncodingException:" + encode);
                            paramsMap.put(paramName, arg.toString());
                        }
                        paramsMapOri.put(paramName, arg.toString());
                    } else if ("$paramMap".equals(paramName)) {
                        Map<String, String> paramMap = (Map<String, String>) arg;
                        paramsMap.putAll(paramMap);
                        paramsMapOri.putAll(paramMap);
                    } else {
                        paramsMap.put(paramName, arg.toString());
                        paramsMapOri.put(paramName, arg.toString());
                    }
                } else {
                    paramsMap.put(paramName, "");
                    paramsMapOri.put(paramName, "");
                }
            }
        }
        return paramsMap;
    }

}
